package com.svndiffdoc;

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.tmatesoft.svn.core.SVNDepth;
import org.tmatesoft.svn.core.SVNDirEntry;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNLogEntry;
import org.tmatesoft.svn.core.SVNNodeKind;
import org.tmatesoft.svn.core.SVNProperties;
import org.tmatesoft.svn.core.SVNProperty;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
import org.tmatesoft.svn.core.internal.io.dav.DAVRepositoryFactory;
import org.tmatesoft.svn.core.internal.wc.SVNPath;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
import org.tmatesoft.svn.core.wc.SVNClientManager;
import org.tmatesoft.svn.core.wc.SVNDiffClient;
import org.tmatesoft.svn.core.wc.SVNDiffStatus;
import org.tmatesoft.svn.core.wc.SVNLogClient;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc.SVNStatusType;
import org.tmatesoft.svn.core.wc.SVNWCUtil;

public class SVNDiffDoc {
    private static Log log = LogFactory.getLog(SVNDiffDoc.class);
    private static final Pattern urlWithRevisionPattern = Pattern.compile("(.*)@(.*)");

    private final String oldRepositoryUrl;
    private final SVNRevision oldRepositoryUrlRevision;
    private final String newRepositoryUrl;
    private final SVNRevision newRepositoryUrlRevision;

    private final SVNRepository newRepository;
    private final SVNRepository oldRepository;

    private final ISVNAuthenticationManager authenticationManager;
    private final SVNClientManager svnClientManager;
    private final SVNDiffClient svnDiffClient;
    private final SVNLogClient svnLogClient;

    private final Map<SVNRepository, Map<Long, SVNLogEntry>> repositoryLogEntriesMap;

    @SuppressWarnings("unchecked")
    public SVNDiffDoc(String oldRepositoryUrlWithOptionalRevision, String newRepositoryUrlWithOptionalRevision, String username, String password) throws SVNException {
        this.oldRepositoryUrl = getRepositoryUrl(oldRepositoryUrlWithOptionalRevision);
        this.oldRepositoryUrlRevision = getRepositoryUrlRevision(oldRepositoryUrlWithOptionalRevision);
        this.newRepositoryUrl = getRepositoryUrl(newRepositoryUrlWithOptionalRevision);
        this.newRepositoryUrlRevision = getRepositoryUrlRevision(newRepositoryUrlWithOptionalRevision);

        DAVRepositoryFactory.setup();

        authenticationManager = createAuthenticationManagerIfNeeded(username, password);
        oldRepository = createRepository(oldRepositoryUrl, authenticationManager);
        newRepository = createRepository(newRepositoryUrl, authenticationManager);

        svnClientManager = SVNClientManager.newInstance(null, authenticationManager);
        svnDiffClient = svnClientManager.getDiffClient();
        svnLogClient = svnClientManager.getLogClient();

        repositoryLogEntriesMap = new HashMap<SVNRepository, Map<Long, SVNLogEntry>>();
        repositoryLogEntriesMap.put(oldRepository, getLogEntryMap(oldRepository.log(new String[] {""}, null, 1, oldRepositoryUrlRevision.getNumber(), true, false)));
        repositoryLogEntriesMap.put(newRepository, getLogEntryMap(newRepository.log(new String[] {""}, null, 1, newRepositoryUrlRevision.getNumber(), true, false)));

    }

    private String getRepositoryUrl(String repositoryUrlWithOptionalRevision) {
        Matcher matcher = urlWithRevisionPattern.matcher(repositoryUrlWithOptionalRevision);
        boolean matchFound = matcher.find();
        if (matchFound) {
            return matcher.group(1);
        }
        return repositoryUrlWithOptionalRevision;
    }

    private SVNRevision getRepositoryUrlRevision(String repositoryUrlWithOptionalRevision) {
        Matcher matcher = urlWithRevisionPattern.matcher(repositoryUrlWithOptionalRevision);
        boolean matchFound = matcher.find();
        if (matchFound) {
            return getRevisionFromString(matcher.group(2));
        }
        return SVNRevision.HEAD;
    }

    private SVNRevision getRevisionFromString(String revision) {
        try {
            return SVNRevision.create(Long.valueOf(revision));
        } catch (NumberFormatException e) {
            throw new RuntimeException("Incorrect revision number '" + revision + "'.");
        }
    }

    private ISVNAuthenticationManager createAuthenticationManagerIfNeeded(String username, String password) {
        if ((username != null) && !username.isEmpty()) {
            return SVNWCUtil.createDefaultAuthenticationManager(username, password);
        }
        return null;
    }

    private SVNRepository createRepository(String repositoryUrl, ISVNAuthenticationManager authenticationManager) throws SVNException {
        SVNRepository repository = SVNRepositoryFactory.create(SVNURL.parseURIDecoded(repositoryUrl));
        repository.setAuthenticationManager(authenticationManager);
        SVNNodeKind nodeKind = repository.checkPath("",  -1);
        if (nodeKind == SVNNodeKind.NONE) {
            throw new RuntimeException("There is no entry at '" + repositoryUrl + "'.");
        } else if (nodeKind == SVNNodeKind.FILE) {
            throw new RuntimeException("The entry at '" + repositoryUrl + "' is a file while a directory was expected.");
        }
        return repository;
    }

    private Map<Long, SVNLogEntry> getLogEntryMap(Collection<SVNLogEntry> logEntries) {
        Map<Long, SVNLogEntry> map = new HashMap<Long, SVNLogEntry>();
        for (SVNLogEntry logEntry : logEntries) {
            map.put(logEntry.getRevision(), logEntry);
        }
        return map;
    }

    public List<SVNDiffStatus> getDifferedObjects() throws SVNException {
        DiffStatusHandler svnDiffStatusHandler = new DiffStatusHandler();

        SVNPath oldRepositoryPath = new SVNPath(oldRepositoryUrl);
        SVNPath newRepositoryPath = new SVNPath(newRepositoryUrl);
        svnDiffClient.doDiffStatus(oldRepositoryPath.getURL(), oldRepositoryUrlRevision, newRepositoryPath.getURL(), newRepositoryUrlRevision, SVNDepth.UNKNOWN, false, svnDiffStatusHandler);

        // Unfold deleted dirs
        return unfoldDeletedDirectories(oldRepository, svnDiffStatusHandler.getSVNDiffStatusList());
    }

    private List<SVNDiffStatus> unfoldDeletedDirectories(SVNRepository repository, List<SVNDiffStatus> differedObjects) throws SVNException {
        List<SVNDiffStatus> list = new ArrayList<SVNDiffStatus>();
        Iterator<SVNDiffStatus> iterator = differedObjects.iterator();
        while (iterator.hasNext()) {
            SVNDiffStatus differedObject = iterator.next();
            if ((differedObject.getKind() == SVNNodeKind.DIR) && (differedObject.getModificationType() == SVNStatusType.STATUS_DELETED)) {
                list.addAll(unfoldDeletedDirectory(repository, differedObject.getPath()));
            }
            list.add(differedObject);
        }
        return list;
    }

    @SuppressWarnings("unchecked")
    public List<SVNDiffStatus> unfoldDeletedDirectory(SVNRepository repository, String path) throws SVNException {
        List<SVNDiffStatus> list = new ArrayList<SVNDiffStatus>();
        Collection<SVNDirEntry> entries = repository.getDir(path, -1, null, (Collection<SVNDirEntry>) null);
        Iterator<SVNDirEntry> iterator = entries.iterator();
        while (iterator.hasNext()) {
            SVNDirEntry entry = iterator.next();
            String entryPath = (path.equals("")) ? entry.getName() : path + "/" + entry.getName();
            if (entry.getKind() == SVNNodeKind.FILE) {
                list.add(new SVNDiffStatus(null, entry.getURL(), entryPath, SVNStatusType.STATUS_DELETED, false, SVNNodeKind.FILE));
            }
            else if (entry.getKind() == SVNNodeKind.DIR) {
                list.addAll(unfoldDeletedDirectory(repository, entryPath));
                list.add(new SVNDiffStatus(null, entry.getURL(), entryPath, SVNStatusType.STATUS_DELETED, false, SVNNodeKind.DIR));
            }
        }
        return list;
    }

    public DiffView getDiffView(SVNDiffStatus differedObject) throws SVNException {
        if (differedObject.getModificationType() == SVNStatusType.STATUS_DELETED) {
            AnnotatedFile annotatedOldFile = annotateFile(oldRepository, differedObject.getPath(), oldRepositoryUrlRevision);
            return new FileDiff(annotatedOldFile, null).getDiffView();
        }
        else if (differedObject.getModificationType() == SVNStatusType.STATUS_MODIFIED) {
            AnnotatedFile annotatedOldFile = annotateFile(oldRepository, differedObject.getPath(), oldRepositoryUrlRevision);
            AnnotatedFile annotatedNewFile = annotateFile(newRepository, differedObject.getPath(), newRepositoryUrlRevision);
            return new FileDiff(annotatedOldFile, annotatedNewFile).getDiffView();
        }
        else if (differedObject.getModificationType() == SVNStatusType.STATUS_ADDED) {
            AnnotatedFile annotatedNewFile = annotateFile(newRepository, differedObject.getPath(), newRepositoryUrlRevision);
            return new FileDiff(null, annotatedNewFile).getDiffView();
        }
        throw new RuntimeException("Unsupported modification type");
    }

    private AnnotatedFile annotateFile(SVNRepository repository, String path, SVNRevision revision) throws SVNException {
        SVNPath filePath = new SVNPath(repository.getLocation().toString() + "/" + path, true);

        SVNProperties fileProperties = new SVNProperties();
        repository.getFile(path, revision.getNumber(), fileProperties, null);

        AnnotatedFile annotatedFile = new AnnotatedFile();

        if (!"application/octet-stream".equals(fileProperties.getStringValue(SVNProperty.MIME_TYPE))) {
            svnLogClient.doAnnotate(filePath.getURL(), filePath.getPegRevision(), SVNRevision.create(1), revision, false, false, annotatedFile, null);

            // Fill message logs
            for (AnnotatedFileLine line : annotatedFile.getLines()) {
                SVNLogEntry logEntry = repositoryLogEntriesMap.get(repository).get(line.getRevision());
                if (logEntry != null) {
                    line.setMessageLog(logEntry.getMessage());
                }
            }
        }

        return annotatedFile;
    }

    public String getOldRepositoryUrl() {
        return oldRepositoryUrl;
    }

    public String getOldRepositoryUrlRevision() {
        return oldRepositoryUrlRevision.toString();
    }

    public String getNewRepositoryUrl() {
        return newRepositoryUrl;
    }

    public String getNewRepositoryUrlRevision() {
        return newRepositoryUrlRevision.toString();
    }


    public static void main(String[] args) {
        try {
            CommandLineParser commandLineParser = new CommandLineParser(args);

            log.info("SVN authorization");
            SVNDiffDoc svnDiffDoc = new SVNDiffDoc(commandLineParser.getOldRepositoryUrlWithOptionalRevision(), commandLineParser.getNewRepositoryUrlWithOptionalRevision(), commandLineParser.getUsername(), commandLineParser.getPassword());

            log.info("Generate summarize diff");
            List<SVNDiffStatus> differedObjects = svnDiffDoc.getDifferedObjects();

            Map<String, String> reportParams = new HashMap<String, String>();
            reportParams.put("svnDiffDocSiteUrl", "http://code.google.com/p/svndiffdoc");
            reportParams.put("svnDiffDocVersion", "1.0.0");
            reportParams.put("reportGenerationDate", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
            reportParams.put("oldRepositoryUlr", svnDiffDoc.getOldRepositoryUrl());
            reportParams.put("oldRepositoryUrlRevision", svnDiffDoc.getOldRepositoryUrlRevision());
            reportParams.put("newRepositoryUlr", svnDiffDoc.getNewRepositoryUrl());
            reportParams.put("newRepositoryUrlRevision", svnDiffDoc.getNewRepositoryUrlRevision());
            HTMLReport htmlReport = new HTMLReport(reportParams);

            // Generate file templates
            Map<String, List<String>> dirFileMap = new HashMap<String, List<String>>();
            for (SVNDiffStatus differedObject : differedObjects) {
                File tempFile = new File(differedObject.getPath());
                String parentDir = tempFile.getParent() == null ? "" : tempFile.getParent().replace("\\", "/");

                List<String> parentDirFileList = dirFileMap.get(parentDir);
                if (parentDirFileList == null) {
                    parentDirFileList = new ArrayList<String>();
                    dirFileMap.put(parentDir, parentDirFileList);
                }

                if (differedObject.getKind() == SVNNodeKind.FILE) {
                    parentDirFileList.add(differedObject.getPath());
                    log.info("Generate diff for file: '" + differedObject.getPath() + "'");
                    DiffView diffView = svnDiffDoc.getDiffView(differedObject);
                    htmlReport.generateFileDiffSummary(differedObject, diffView);
                }
            }

            // Generate dir templates
            for (String dir : dirFileMap.keySet()) {
                log.info("Generate file list for dir: '" + dir + "'");
                htmlReport.generateDirFilesList(dir, dirFileMap.get(dir));
                log.info("Generate summary for dir: '" + dir + "'");
                htmlReport.generateDirSummary(dir);
            }

            // Generate remaining templates
            log.info("Generate all files list");
            htmlReport.generateAllFilesList(dirFileMap);
            log.info("Generate all dirs list");
            htmlReport.generateAllDirsList(dirFileMap);
            htmlReport.generateReportSummary();
            htmlReport.generateIndexHtml();
            htmlReport.generateStaticFiles();

            log.info("Done");
        }
        catch (Exception e) {
            System.out.println(e.getLocalizedMessage());
        }
    }

}
